package org.activision.io;

import org.activision.util.Misc;

public final class OutStream {

    private static final int[] BIT_MASK = new int[32];
    private int offset = 0;
    private int length = 0;
    private byte[] buffer;
    private int bitPosition = 0;
    private int opcodeStart = 0;

    public OutStream(int capacity) {
        setBuffer(new byte[capacity]);
    }
    public OutStream() {
        setBuffer(new byte[16]);
    }

    public OutStream(byte[] buffer) {
        this.setBuffer(buffer);
    }

    public void checkCapacityPosition(int position) {
        if (position >= getBuffer().length) {
            byte[] newBuffer = new byte[position + 16];
            System.arraycopy(getBuffer(), 0, newBuffer, 0, getBuffer().length);
            setBuffer(newBuffer);
        }
    }

    public void skip(int length) {
        setOffset(getOffset() + length);
    }

    public int offset() {
        return getOffset();
    }

    public int length() {
        return length;
    }

    public void setOffset(int offset) {
        this.offset = offset;
    }

    public byte[] buffer() {
        return getBuffer();
    }

    public void writeBytes(byte[] b, int offset, int length) {
        checkCapacityPosition(this.getOffset() + length - offset);
        System.arraycopy(b, offset, getBuffer(), this.getOffset(), length);
        this.setOffset(this.getOffset() + (length - offset));
    }

    public void addBytes(byte[] b) {
        int offset = 0;
        int length = b.length;
        checkCapacityPosition(this.getOffset() + length - offset);
        System.arraycopy(b, offset, getBuffer(), this.getOffset(), length);
        this.setOffset(this.getOffset() + (length - offset));
    }

    public void addBytes128(byte[] data, int offset, int len) {
        for (int k = offset; k < len; k++)
            writeByte((byte) (data[k] + 128));
    }

    public void addBytesS(byte[] data, int offset, int len) {
        for (int k = offset; k < len; k++)
            writeByte((byte) (-128 + data[k]));
    }

    public void addBytes_Reverse(byte[] data, int offset, int len) {
        for (int i = len - 1; i >= 0; i--) {
            writeByte((byte) (data[i]));
        }
    }

	public void addBytes_Reverse128(byte[] data, int offset, int len) {
    	for (int i = len - 1; i >= 0; i--) {
    		writeByte((byte) (data[i] + 128));
    	}
	}

    public void writeByte(int i) {
        writeByte(i, offset++);
    }

    public void writeNegativeByte(int i) {
        writeByte(-i, offset++);
    }

    public void writeByte(int i, int position) {
        checkCapacityPosition(position);
        getBuffer()[position] = (byte) i;
    }

    public void writeByte128(int i) {
        writeByte(i + 128);
    }

    public void writeByteC(int i) {
        writeByte(-i);
    }

    public void write3Byte(int i) {
        writeByte(i >> 16);
        writeByte(i >> 8);
        writeByte(i);
    }

    public void write128Byte(int i) {
        writeByte(128 - i);
    }

    public void writeShortLE128(int i) {
        writeByte(i + 128);
        writeByte(i >> 8);
    }

    public void writeShort128(int i) {
        writeByte(i >> 8);
        writeByte(i + 128);
    }

    public void writeSmart(int i) {
        if (i >= 128) {
            writeShort(i + 32768);
        } else {
            writeByte(i);
        }
    }

    public void writeShort(int i) {
        writeByte(i >> 8);
        writeByte(i);
    }

    public void writeShortLE(int i) {
        writeByte(i);
        writeByte(i >> 8);
    }

    public void write3Bytes(int i) {
        writeByte(i >> 16);
        writeByte(i >> 8);
        writeByte(i);
    }

    public void writeInt(int i) {
        writeByte(i >> 24);
        writeByte(i >> 16);
        writeByte(i >> 8);
        writeByte(i);
    }

    public void writeIntV1(int i) {
        writeByte(i >> 8);
        writeByte(i);
        writeByte(i >> 24);
        writeByte(i >> 16);
    }

    public void writeIntV2(int i) {
        writeByte(i >> 16);
        writeByte(i >> 24);
        writeByte(i);
        writeByte(i >> 8);
    }

    public void writeIntLE(int i) {
        writeByte(i);
        writeByte(i >> 8);
        writeByte(i >> 16);
        writeByte(i >> 24);
    }

    public void writeLong(long l) {
        writeByte((int) (l >> 56));
        writeByte((int) (l >> 48));
        writeByte((int) (l >> 40));
        writeByte((int) (l >> 32));
        writeByte((int) (l >> 24));
        writeByte((int) (l >> 16));
        writeByte((int) (l >> 8));
        writeByte((int) l);
    }

    public void writePSmarts(int i) {
        if (i < 128) {
            writeByte(i);
            return;
        }
        if (i < 32768) {
            writeShort(32768 + i);
            return;
        } else {
            System.out.println("Error psmarts out of range:");
            return;
        }
    }

    public void writeString(String s) {
        checkCapacityPosition(getOffset() + s.length() + 1);
        System.arraycopy(s.getBytes(), 0, getBuffer(), getOffset(), s.length());
        setOffset(getOffset() + s.length());
        writeByte(0);
    }

    public void putGJString2(String string) {
        byte[] packed = new byte[256];
        int length = Misc.packGJString2(0, packed, string);
        writeByte(0);
        writeBytes(packed, 0, length);
        writeByte(0);
    }

    public void putGJString(String s) {
        writeByte(0);
        writeString(s);
    }

    public void writePacket(int id) {
        writeByte(id);
    }

    public void writePacketVarByte(int id) {
        writePacket(id);
        writeByte(0);
        opcodeStart = getOffset() - 1;
    }

    public void writePacketVarShort(int id) {
        writePacket(id);
        writeShort(0);
        opcodeStart = getOffset() - 2;
    }

    /*public void writePacketShort(int id) {
         writeByte(id);
         writeShort(0);
         opcodeStart = getOffset() - 2;
     }*/

    public void endPacketVarByte() {
        writeByte(getOffset() - (opcodeStart + 2) + 1, opcodeStart);
    }

    public void endPacketVarShort() {
        int size = getOffset() - (opcodeStart + 2);
        writeByte(size >> 8, opcodeStart++);
        writeByte(size, opcodeStart);
    }

    public void initBitAccess() {
        bitPosition = getOffset() * 8;
    }

    public void finishBitAccess() {
        setOffset((bitPosition + 7) / 8);
    }

    public int getBitPos(int i) {
        return 8 * i - bitPosition;
    }

    public void writeBits(int numBits, int value) {
        int bytePos = bitPosition >> 3;
        int bitOffset = 8 - (bitPosition & 7);
        bitPosition += numBits;
        for (; numBits > bitOffset; bitOffset = 8) {
            checkCapacityPosition(bytePos);
            getBuffer()[bytePos] &= ~BIT_MASK[bitOffset];
            getBuffer()[bytePos++] |= value >> numBits - bitOffset & BIT_MASK[bitOffset];
            numBits -= bitOffset;
        }
        checkCapacityPosition(bytePos);
        if (numBits == bitOffset) {
            getBuffer()[bytePos] &= ~BIT_MASK[bitOffset];
            getBuffer()[bytePos] |= value & BIT_MASK[bitOffset];
        } else {
            getBuffer()[bytePos] &= ~(BIT_MASK[numBits] << bitOffset - numBits);
            getBuffer()[bytePos] |= (value & BIT_MASK[numBits]) << bitOffset - numBits;
        }
    }


    public byte[] getBuffer() {
        return buffer;
    }

    public int getOffset() {
        return offset;
    }


    public void setBuffer(byte[] buffer) {
        this.buffer = buffer;
    }

    static {
        for (int i = 0; i < 32; i++) {
            BIT_MASK[i] = (1 << i) - 1;
        }
    }
    /**
     * Writes an object to the target stream.
     *
     * @param object
     *            the object to write to the target stream.
     * @throws IOException
     *             if an error occurs while writing to the target stream.
     * @see ObjectInputStream#readObject()
     */

    /**
     * Writes an unshared object to the target stream. This method is identical
     * to {@code writeObject}, except that it always writes a new object to the
     * stream versus the use of back-referencing for identical objects by
     * {@code writeObject}.
     *
     * @param object
     *            the object to write to the target stream.
     * @throws IOException
     *             if an error occurs while writing to the target stream.
     * @see ObjectInputStream#readUnshared()
     */
}
